import pandas as pd
import ping3
import tkinter as tk
from tkinter import ttk, filedialog
import threading
from tkinter import messagebox
import concurrent.futures
import openpyxl
from openpyxl.styles import Font, Alignment
import datetime
import io
import schedule
import time
import winsound
from PIL import Image, ImageDraw
import pystray
import os
import json
import ipaddress

class NetworkCheckerApp:
    STATUS_COLUMN = 3  # 状态列的索引
    
    def __init__(self):
        # 隐藏控制台窗口（仅Windows有效）
        import ctypes
        ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)
        
        self.root = tk.Tk()
        self.root.title("网络连通性检查工具 v1.4")
        self.root.geometry("800x600")  # 加大窗口
        # 设置样式
        style = ttk.Style()
        style.theme_use('clam')  # 或 'alt', 'default', 'classic'
        # 设置Treeview样式
        style.map('Treeview', 
            foreground=[('selected', '#000000')],
            background=[('selected', '#CCE5FF')]
        )
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        # 顶部信息框架
        info_frame = ttk.Frame(main_frame)
        info_frame.pack(fill=tk.X, pady=(0, 10))
        # 添加统计信息标签
        self.status_label = ttk.Label(info_frame, text="就绪")
        self.status_label.pack(side=tk.LEFT)
        # 进度条
        self.progress = ttk.Progressbar(info_frame, mode='determinate')
        self.progress.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=10)
        # 按钮框架
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=(0, 10))
        # 美化按钮
        self.load_btn = ttk.Button(btn_frame, text="📂 导入Excel", command=self.load_excel)
        self.check_btn = ttk.Button(btn_frame, text="▶ 开始检测", command=self.start_check)
        self.stop_btn = ttk.Button(btn_frame, text="⏹ 停止检测", command=self.stop_check, state='disabled')
        self.export_btn = ttk.Button(btn_frame, text="💾 导出结果", command=self.export_result)
        self.schedule_btn = ttk.Button(btn_frame, text="⏰ 定时检测", command=self.show_schedule_dialog)
        # 声音设置
        self.alarm_enabled = tk.BooleanVar(value=True)
        self.sound_file = None  # 自定义声音文件路径
        # 在按钮框架中添加报警设置
        alarm_frame = ttk.Frame(btn_frame)
        alarm_frame.pack(side=tk.LEFT, padx=5)
        alarm_btn = ttk.Checkbutton(alarm_frame, text="声音报警", variable=self.alarm_enabled)
        alarm_btn.pack(side=tk.LEFT)
        ttk.Button(alarm_frame, text="选择声音", command=self.choose_sound).pack(side=tk.LEFT, padx=2)
        # 表格
        self.tree = ttk.Treeview(main_frame, 
            columns=('序号', 'IP', '备注', '状态', '延迟', '检测时间'), 
            show='headings')
        # 设置表头
        for col in ['序号', 'IP', '备注', '状态', '延迟', '检测时间']:
            self.tree.heading(col, 
                text=col,
                command=lambda c=col: self.tree_sort_column(c, False))
        # 设置列宽和对齐
        self.tree.column('序号', width=50, anchor='center')
        self.tree.column('IP', width=150, anchor='center')
        self.tree.column('备注', width=200, anchor='w')
        self.tree.column('状态', width=100, anchor='center')
        self.tree.column('延迟', width=100, anchor='center')
        self.tree.column('检测时间', width=150, anchor='center')
        # 添加滚动条
        scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        # 布局
        self.load_btn.pack(side=tk.LEFT, padx=5)
        self.check_btn.pack(side=tk.LEFT, padx=5)
        self.stop_btn.pack(side=tk.LEFT, padx=5)
        self.export_btn.pack(side=tk.LEFT, padx=5)
        self.schedule_btn.pack(side=tk.LEFT, padx=5)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        # 版权信息框架
        copyright_frame = ttk.Frame(main_frame)
        copyright_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(5, 0))
        version_label = ttk.Label(copyright_frame, 
            text="版本: v1.4", 
            font=('微软雅黑', 8))
        version_label.pack(side=tk.LEFT)
        copyright_label = ttk.Label(copyright_frame, 
            text="© 2024 甘肃传德源科技工程有限公司", 
            font=('微软雅黑', 8))
        copyright_label.pack(side=tk.RIGHT)
        # 添加停止标志
        self.is_checking = False
        # 添加右键菜单
        self.context_menu = tk.Menu(self.root, tearoff=0)
        self.context_menu.add_command(label="重新检测选中项", command=self.recheck_selected)
        self.context_menu.add_command(label="复制IP地址", command=self.copy_ip)
        self.context_menu.add_separator()
        self.context_menu.add_command(label="清除所有结果", command=self.clear_results)
        self.tree.bind("<Button-3>", self.show_context_menu)
        # 添加定时任务状态标签
        self.schedule_label = ttk.Label(info_frame, text="")
        self.schedule_label.pack(side=tk.LEFT, padx=10)
        # 定时任务变量
        self.scheduled_time = None
        self.schedule_thread = None
        # 添加系统托盘相关变量
        self.icon = None
        self.create_tray_icon()
        # 绑定窗口关闭事件
        self.root.protocol('WM_DELETE_WINDOW', self.hide_window)
        # 声音设置
        self.enable_sound = tk.BooleanVar(value=True)
        # 在菜单中添加声音设置
        self.context_menu.add_separator()
        self.context_menu.add_checkbutton(
            label="启用声音提醒", 
            variable=self.enable_sound)
        # 添加线程池配置
        self.max_workers = min(32, os.cpu_count() * 2)  # 根据CPU核心数设置最大线程数
        self.batch_size = 50  # 每批处理的IP数量
        # 加载设置
        self.load_settings_from_file()
        # 在程序退出时保存设置
        self.root.protocol('WM_DELETE_WINDOW', self.on_closing)
    def load_excel(self):
        try:
            file_path = filedialog.askopenfilename(
                title="选择Excel文件",
                filetypes=[("Excel文件", "*.xlsx;*.xls"), ("所有文件", "*.*")]
            )
            if not file_path:
                return
            self.df = pd.read_excel(file_path)
            # 检查列名，支持多种可能的列名格式
            ip_columns = ['IP address', 'IP地址', 'IP', 'ip', 'ip地址', 'IP_address']
            remark_columns = ['备注', '说明', 'remark', 'Remark', 'Comment', 'comment']
            # 查找IP地址列
            ip_col = None
            for col in ip_columns:
                if col in self.df.columns:
                    ip_col = col
                    break
            # 查找备注列
            remark_col = None
            for col in remark_columns:
                if col in self.df.columns:
                    remark_col = col
                    break
            if ip_col is None or remark_col is None:
                tk.messagebox.showerror(
                    "格式错误",
                    f"Excel文件格式错误！\n\n"
                    f"请确保Excel文件包含以下列：\n"
                    f"1. IP地址列（可用名称：{', '.join(ip_columns)}）\n"
                    f"2. 备注列（可用名称：{', '.join(remark_columns)}）\n\n"
                    f"当前文件的列：{', '.join(self.df.columns)}"
                )
                return
            # 验证IP地址格式
            invalid_ips = []
            for idx, row in self.df.iterrows():
                ip = str(row[ip_col]).strip()
                if not self.is_valid_ip(ip):
                    invalid_ips.append(f"行 {idx+2}: {ip}")
            if invalid_ips:
                tk.messagebox.showwarning(
                    "IP地址格式错误",
                    f"以下IP地址格式无效：\n\n{chr(10).join(invalid_ips[:5])}"
                    f"\n{'...' if len(invalid_ips) > 5 else ''}"
                )
            # 清空现有数据
            for i in self.tree.get_children():
                self.tree.delete(i)
            # 加载数据到界面，添加序号并过滤IP
            valid_count = 0
            total_generated = 0  # 总生成的IP数量
            ignored_count = 0    # 被过滤的IP数量
            
            for idx, row in enumerate(self.df.iterrows(), 1):
                _, row_data = row
                raw_ip = str(row_data[ip_col]).strip()
                
                # 解析IP地址范围
                generated_ips = self.parse_ip_range(raw_ip)
                total_generated += len(generated_ips)
                
                for ip in generated_ips:
                    # 过滤保留地址（最后一位为0或255）
                    try:
                        last_octet = int(ip.split('.')[-1])
                        if last_octet == 0 or last_octet == 255:
                            ignored_count +=1
                            continue
                    except (ValueError, IndexError):
                        ignored_count +=1
                        continue
                    
                    # 检查IP有效性
                    if not self.is_valid_ip(ip):
                        ignored_count +=1
                        continue
                    
                    valid_count += 1
                    item = self.tree.insert('', 'end', values=(
                        valid_count,  # 使用有效计数作为序号
                        ip,  # IP地址
                        row_data[remark_col],  # 备注
                        '待检测',  # 状态
                        '',  # 延迟
                        ''  # 检测时间
                    ))
                    self.tree.tag_configure(item, foreground='gray')
                    self.tree.item(item, tags=(item,))
            
            # 更新统计信息
            self.update_statistics()
            messagebox.showinfo(
                "导入完成",
                f"成功导入{valid_count}条有效IP地址\n" 
                f"共生成{total_generated}个IP地址\n"
                f"过滤{ignored_count}个无效或保留地址"
            )
        except Exception as e:
            tk.messagebox.showerror("错误", f"读取Excel文件失败：\n{str(e)}")
    def is_valid_ip(self, ip):
        """验证IP地址格式"""
        try:
            parts = ip.split('.')
            return len(parts) == 4 and all(0 <= int(part) <= 255 for part in parts)
        except:
            return False
    def check_ip(self, item):
        """检测单个IP的连通性（增强版）"""
        if not self.is_checking or not self.tree.exists(item):
            return
        
        try:
            values = self.tree.item(item)['values']
            if len(values) < 6:
                return
            
            full_ip = values[1]
            # 增强IP格式处理
            if '. ' in full_ip:
                ip_part = full_ip.split('. ', 1)
                if len(ip_part) == 2 and ip_part[0].isdigit():
                    ip = ip_part[1]
                else:
                    ip = full_ip
            else:
                ip = full_ip
            
            if not self.is_valid_ip(ip):
                return
            
            delay = ping3.ping(ip, timeout=2)
            status = '在线' if delay else '离线'
            return (item, full_ip, values[2], status, delay)
        except Exception as e:
            return (item, full_ip, values[2], f'检测失败: {str(e)}', None)
    def update_ui(self, item, status, delay, current_count, total_count):
        """安全地更新UI"""
        if not self.root.winfo_exists() or not self.tree.exists(item):
            return
        try:
            # 获取之前的状态
            prev_status = self.tree.item(item)['values'][2]
            # 设置行颜色
            if '在线' in status:
                self.tree.tag_configure(item, foreground='green')
                if prev_status != '待检测' and '离线' in prev_status:
                    self.play_sound('online')
            elif '离线' in status:
                self.tree.tag_configure(item, foreground='red')
                if prev_status != '待检测' and '在线' in prev_status:
                    self.play_sound('offline')
            else:  # 检测失败
                self.tree.tag_configure(item, foreground='orange')
            # 添加检测时间
            check_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            self.tree.item(item, values=(
                self.tree.item(item)['values'][0],  # 序号
                self.tree.item(item)['values'][1],  # IP地址
                self.tree.item(item)['values'][2],  # 备注
                status,  # 更新状态
                f'{delay:.2f}ms' if delay else '',  # 延迟
                check_time  # 检测时间
            ), tags=(item,))
            # 自动滚动到当前检测项
            self.tree.see(item)
            self.progress['value'] = (current_count / total_count) * 100
            self.root.update_idletasks()
            # 检测完成时处理
            if current_count == total_count:
                self.play_sound('complete')
                # 获取所有离线IP（索引3是状态列）
                offline_items = [item for item in self.tree.get_children()
                               if '离线' in self.tree.item(item)['values'][3]]
                print(f"离线IP数量: {len(offline_items)}")
                if offline_items:
                    print("准备显示离线IP报警窗口")
                    self.root.after(0, lambda: self.show_offline_alert(offline_items))
        except Exception as e:
            print(f"UI更新错误: {str(e)}")
    def start_check(self):
        """执行网络检测"""
        if self.is_checking:  # 防止重复启动
            return
        def check_thread():
            self.is_checking = True
            items = self.tree.get_children()
            total = len(items)
            current = 0
            self.check_btn.config(state='disabled')
            self.stop_btn.config(state='normal')
            self.load_btn.config(state='disabled')
            self.export_btn.config(state='disabled')
            # 重置进度条
            self.progress['value'] = 0
            try:
                # 分批处理
                for i in range(0, len(items), self.batch_size):
                    if not self.is_checking:
                        break
                    batch_items = items[i:i + self.batch_size]
                    # 使用线程池并发检测当前批次
                    with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
                        futures = [executor.submit(self.check_ip, item) for item in batch_items]
                        for future in concurrent.futures.as_completed(futures):
                            if not self.is_checking:
                                executor.shutdown(wait=False)
                                break
                            try:
                                result = future.result()
                                if result:
                                    item, ip, remark, status, delay = result
                                    current += 1
                                    self.root.after(0, self.update_ui, item, status, delay, current, total)
                                    self.root.after(0, lambda: self.root.title(
                                        f"网络连通性检查工具 - 进度: {current}/{total}"))
                            except Exception as e:
                                print(f"检测错误: {str(e)}")
                                continue
                    # 每批处理完后暂停一小段时间，避免系统负载过高
                    time.sleep(0.1)
            finally:
                # 恢复按钮状态
                self.root.after(0, lambda: [
                    self.check_btn.config(state='normal'),
                    self.stop_btn.config(state='disabled'),
                    self.load_btn.config(state='normal'),
                    self.export_btn.config(state='normal'),
                    self.root.title("网络连通性检查工具 v1.4"),
                    self.update_statistics()
                ])
                
                # 获取检测结果
                offline_items = [item for item in self.tree.get_children()
                               if '离线' in self.tree.item(item)['values'][3]]
                print(f"[检测完成] 离线IP数量：{len(offline_items)}")
                
                # 使用线程安全的方式处理结果
                self.root.after(0, lambda: self.finalize_check(offline_items))
                self.is_checking = False
        # 启动检测线程
        threading.Thread(target=check_thread, daemon=True).start()
    def finalize_check(self, offline_items):
        """安全完成检测流程"""
        try:
            self.update_statistics()
            if offline_items:
                # 强制播放报警声
                self.play_sound('offline', force=True)
                self.show_offline_alert(offline_items)
            else:
                self.play_sound('complete')
            self.is_checking = False
        except Exception as e:
            print(f"完成检测时出错: {str(e)}")
    def stop_check(self):
        """停止网络检测"""
        self.is_checking = False
        self.stop_btn.config(state='disabled')
    def export_result(self):
        """导出结果到Excel"""
        try:
            output_path = filedialog.asksaveasfilename(
                title="导出结果",
                defaultextension=".xlsx",
                filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
            )
            if not output_path:
                return
            # 获取所有数据并添加序号
            data = []
            for item in self.tree.get_children():
                values = self.tree.item(item)['values']
                # 修改点：直接使用完整IP地址（保留序号前缀）
                data.append([
                    values[0],  # 序号
                    values[1],  # 完整IP地址（包含序号前缀）
                    values[2],  # 备注
                    values[3],  # 状态
                    values[4],  # 延迟
                    values[5]  # 检测时间
                ])
            df = pd.DataFrame(data, columns=['序号', 'IP地址', '备注', '状态', '延迟', '检测时间'])
            # 创建DataFrame并导出
            with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
                df.to_excel(writer, index=False, sheet_name='检测结果')
                # 获取工作表
                worksheet = writer.sheets['检测结果']
                # 调整列宽
                worksheet.column_dimensions['A'].width = 15  # IP地址
                worksheet.column_dimensions['B'].width = 20  # 备注
                worksheet.column_dimensions['C'].width = 12  # 状态
                worksheet.column_dimensions['D'].width = 12  # 延迟
                worksheet.column_dimensions['E'].width = 15  # 检测时间
                worksheet.column_dimensions['F'].width = 20  # 检测时间列
                # 设置标题行格式
                for cell in worksheet[1]:
                    cell.font = openpyxl.styles.Font(bold=True)
                    cell.alignment = openpyxl.styles.Alignment(horizontal='center')
                # 设置所有单元格居中对齐（除备注外）
                for row in worksheet.iter_rows(min_row=2):
                    for idx, cell in enumerate(row):
                        if idx != 1:  # 不是备注列
                            cell.alignment = openpyxl.styles.Alignment(horizontal='center')
            messagebox.showinfo("成功", "结果已成功导出！")
        except Exception as e:
            messagebox.showerror("错误", f"导出失败：\n{str(e)}")
    def update_statistics(self):
        """更新统计信息"""
        total = len(self.tree.get_children())
        online = sum(1 for item in self.tree.get_children() 
                    if '在线' in self.tree.item(item)['values'][3])  # 确保检查状态列
        offline = sum(1 for item in self.tree.get_children() 
                     if '离线' in self.tree.item(item)['values'][3])  # 确保检查状态列
        failed = sum(1 for item in self.tree.get_children() 
                    if '失败' in self.tree.item(item)['values'][3])  # 确保检查状态列
        stats = f"总计: {total} | 在线: {online} | 离线: {offline} | 失败: {failed}"
        self.status_label.config(text=stats)
    def show_context_menu(self, event):
        """显示右键菜单"""
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
            self.context_menu.post(event.x_root, event.y_root)
    def copy_ip(self):
        """复制选中的IP地址"""
        selected = self.tree.selection()
        if selected:
            ip = self.tree.item(selected[0])['values'][1]
            self.root.clipboard_clear()
            self.root.clipboard_append(ip)
    def recheck_selected(self):
        """重新检测选中的IP"""
        selected = self.tree.selection()
        if not selected:
            return
        def check_selected_thread():
            self.is_checking = True
            total = len(selected)
            current = 0
            self.root.after(0, lambda: self.check_btn.config(state='disabled'))
            self.root.after(0, lambda: self.stop_btn.config(state='normal'))
            try:
                # 分批处理选中项
                for i in range(0, len(selected), self.batch_size):
                    if not self.is_checking:
                        break
                    batch_items = selected[i:i + self.batch_size]
                    with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
                        futures = [executor.submit(self.check_ip, item) for item in batch_items]
                        for future in concurrent.futures.as_completed(futures):
                            if not self.is_checking:
                                executor.shutdown(wait=False)
                                break
                            try:
                                result = future.result()
                                if result:
                                    item, ip, remark, status, delay = result
                                    current += 1
                                    self.root.after(0, self.update_ui, item, status, delay, current, total)
                            except Exception as e:
                                print(f"重新检测错误: {str(e)}")
                                continue
                    time.sleep(0.1)
            finally:
                self.root.after(0, lambda: self.check_btn.config(state='normal'))
                self.root.after(0, lambda: self.stop_btn.config(state='disabled'))
                self.root.after(0, self.update_statistics)
                self.is_checking = False
        threading.Thread(target=check_selected_thread, daemon=True).start()
    def clear_results(self):
        """清除所有检测结果"""
        for item in self.tree.get_children():
            self.tree.tag_configure(item, foreground='gray')  # 重置为灰色
            self.tree.item(item, values=(
                self.tree.item(item)['values'][0],  # 序号
                self.tree.item(item)['values'][1],  # IP地址
                self.tree.item(item)['values'][2],  # 备注
                '待检测',  # 状态
                '',  # 延迟
                ''  # 检测时间
            ), tags=(item,))
        self.update_statistics()
        self.root.title("网络连通性检查工具")
    def show_schedule_dialog(self):
        """显示定时设置对话框"""
        dialog = tk.Toplevel(self.root)
        dialog.title("定时检测设置")
        dialog.geometry("400x300")  # 减小窗口高度
        dialog.transient(self.root)
        # 主框架
        main_frame = ttk.Frame(dialog, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        # 时间设置框架
        time_frame = ttk.LabelFrame(main_frame, text="检测时间", padding="10")
        time_frame.pack(fill=tk.X, pady=5)
        # 获取当前日期时间
        now = datetime.datetime.now()
        # 年月日设置
        date_frame = ttk.Frame(time_frame)
        date_frame.pack(fill=tk.X, pady=5)
        year_var = tk.StringVar(value=str(now.year))
        month_var = tk.StringVar(value=str(now.month))
        day_var = tk.StringVar(value=str(now.day))
        ttk.Label(date_frame, text="年:").pack(side=tk.LEFT)
        year_spin = ttk.Spinbox(date_frame, from_=2024, to=2100, width=6, textvariable=year_var)
        year_spin.pack(side=tk.LEFT, padx=5)
        ttk.Label(date_frame, text="月:").pack(side=tk.LEFT)
        month_spin = ttk.Spinbox(date_frame, from_=1, to=12, width=4, textvariable=month_var)
        month_spin.pack(side=tk.LEFT, padx=5)
        ttk.Label(date_frame, text="日:").pack(side=tk.LEFT)
        day_spin = ttk.Spinbox(date_frame, from_=1, to=31, width=4, textvariable=day_var)
        day_spin.pack(side=tk.LEFT, padx=5)
        # 时分设置
        time_frame2 = ttk.Frame(time_frame)
        time_frame2.pack(fill=tk.X, pady=5)
        hour_var = tk.StringVar(value=str(now.hour))
        minute_var = tk.StringVar(value=str(now.minute))
        ttk.Label(time_frame2, text="时:").pack(side=tk.LEFT)
        hour_spin = ttk.Spinbox(time_frame2, from_=0, to=23, width=4, textvariable=hour_var)
        hour_spin.pack(side=tk.LEFT, padx=5)
        ttk.Label(time_frame2, text="分:").pack(side=tk.LEFT)
        minute_spin = ttk.Spinbox(time_frame2, from_=0, to=59, width=4, textvariable=minute_var)
        minute_spin.pack(side=tk.LEFT, padx=5)
        # 倒计时标签
        self.countdown_label = ttk.Label(main_frame, text="", font=('微软雅黑', 10))
        self.countdown_label.pack(pady=(10, 0))
        # 检测模式选择
        mode_frame = ttk.Frame(time_frame)
        mode_frame.pack(fill=tk.X, pady=5)
        mode_var = tk.StringVar(value="once")
        ttk.Radiobutton(mode_frame, text="单次检测", 
                       variable=mode_var, value="once").pack(side=tk.LEFT)
        ttk.Radiobutton(mode_frame, text="定时循环", 
                       variable=mode_var, value="interval").pack(side=tk.LEFT)
        # 循环间隔设置
        interval_frame = ttk.Frame(time_frame)
        interval_frame.pack(fill=tk.X, pady=5)
        interval_var = tk.StringVar(value="10")
        ttk.Label(interval_frame, text="循环间隔(分钟):").pack(side=tk.LEFT)
        interval_spin = ttk.Spinbox(interval_frame, from_=1, to=1440, 
                                  width=5, textvariable=interval_var)
        interval_spin.pack(side=tk.LEFT, padx=5)
        # 按钮框架
        btn_frame = ttk.Frame(main_frame)
        btn_frame.pack(fill=tk.X, pady=10)
        save_settings_with_args = lambda: self.save_settings(
            dialog, year_var, month_var, day_var, 
            hour_var, minute_var, mode_var, interval_var
        )
        ttk.Button(btn_frame, text="开始定时", command=save_settings_with_args).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="停止定时", command=self.stop_check, state='disabled').pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="取消", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
    def schedule_check(self, schedule_time, mode="once", interval=60):
        """设置定时检测
        mode: 'once' 单次检测, 'interval' 循环检测
        interval: 循环间隔（分钟）
        """
        self.scheduled_time = schedule_time
        self.schedule_mode = mode
        self.schedule_interval = interval
        self.start_countdown(schedule_time)  # 启动倒计时
        def schedule_task():
            while True:
                schedule.run_pending()
                time.sleep(1)  # 每秒检查一次
        # 清除现有的定时任务
        schedule.clear()
        if mode == "once":
            # 单次检测
            schedule.every().day.at(schedule_time.strftime("%H:%M")).do(
                self.run_scheduled_check)
            status = f"定时检测: {schedule_time.strftime('%Y-%m-%d %H:%M')}"
        else:
            # 循环检测
            def run_interval_check():
                self.run_scheduled_check()
                self.update_countdown(self.schedule_interval * 60)  # 更新倒计时
                return schedule.CancelJob  # 取消当前任务
            schedule.every(interval).minutes.do(run_interval_check)
            status = f"循环检测: 每{interval}分钟"
        # 更新状态标签
        self.schedule_label.config(text=status)
        # 启动定时任务线程
        if self.schedule_thread is None or not self.schedule_thread.is_alive():
            self.schedule_thread = threading.Thread(
                target=schedule_task, daemon=True)
            self.schedule_thread.start()
    def start_countdown(self, schedule_time):
        """启动倒计时"""
        now = datetime.datetime.now()
        countdown_seconds = (schedule_time - now).total_seconds()
        if countdown_seconds > 0:
            self.update_countdown(countdown_seconds)
    def update_countdown(self, seconds):
        """更新倒计时标签"""
        if seconds > 0:
            minutes, seconds = divmod(int(seconds), 60)
            if self.countdown_label.winfo_exists():  # 检查标签是否仍然存在
                self.countdown_label.config(text=f"倒计时: {minutes} 分 {seconds} 秒")
            self.root.after(1000, self.update_countdown, seconds - 1)
        else:
            if self.countdown_label.winfo_exists():  # 检查标签是否仍然存在
                self.countdown_label.config(text="倒计时结束！")
    def run_scheduled_check(self):
        """执行定时检测"""
        try:
            self.start_check()
            # 等待检测完成
            while self.is_checking:
                time.sleep(1)
            # 获取离线的IP并报警（修正为检查索引3）
            offline_items = [item for item in self.tree.get_children()
                           if '离线' in self.tree.item(item)['values'][3]]  # 索引3
            print(f"[定时检测] 发现离线IP数量：{len(offline_items)}")
            if offline_items:
                self.root.after(0, lambda: self.show_offline_alert(offline_items))
        except Exception as e:
            print(f"定时检测错误: {str(e)}")
    def show_offline_alert(self, offline_items):
        """显示离线IP报警窗口（线程安全版）"""
        def create_window():
            try:
                if hasattr(self, '_alert_open') and self._alert_open:
                    self.close_alert_window()
                
                # 必须在主线程创建窗口
                self.alert_window = tk.Toplevel(self.root)
                self._alert_open = True
                
                # 窗口基础设置
                self.alert_window.title("⚠ 离线设备报警")
                self.alert_window.geometry("600x400")
                self.alert_window.protocol("WM_DELETE_WINDOW", self.close_alert_window)
                
                # 内容布局
                content_frame = ttk.Frame(self.alert_window)
                content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
                
                # 添加详细列表
                list_frame = ttk.Frame(content_frame)
                list_frame.pack(fill=tk.BOTH, expand=True)

                # 创建Treeview
                columns = ('IP地址', '备注', '最后在线时间')
                tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=8)
                
                # 配置列
                tree.heading('IP地址', text='IP地址')
                tree.heading('备注', text='备注')
                tree.heading('最后在线时间', text='最后在线时间')
                tree.column('IP地址', width=150, anchor='w')
                tree.column('备注', width=200, anchor='w')
                tree.column('最后在线时间', width=200, anchor='w')
                
                # 添加滚动条
                scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=tree.yview)
                tree.configure(yscrollcommand=scrollbar.set)
                
                # 在主线程填充数据
                def load_data():
                    try:
                        for item in offline_items:
                            if not self.tree.exists(item):  # 验证条目有效性
                                continue
                            values = self.tree.item(item)['values']
                            # 提取实际IP地址（去掉序号）
                            full_ip = values[1]
                            ip = full_ip.split('. ', 1)[1] if '. ' in full_ip else full_ip
                            tree.insert('', 'end', values=(
                                ip,
                                values[2],  # 备注
                                values[5]   # 检测时间
                            ))
                    except Exception as e:
                        print(f"加载数据失败: {str(e)}")
                
                # 延迟加载数据确保窗口创建完成
                self.alert_window.after(100, load_data)
                
                # 布局
                tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
                scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
                
                # 统计信息
                ttk.Label(content_frame, 
                        text=f"共发现 {len(offline_items)} 个离线设备", 
                        font=('微软雅黑', 12),
                        foreground='red').pack(pady=5)
                
                ttk.Button(content_frame, 
                         text="确认", 
                         command=lambda: [self.close_alert_window(), 
                                        winsound.PlaySound(None, winsound.SND_PURGE)]
                ).pack(pady=10)
                
                # 在窗口创建后立即播放报警声（强制模式）
                self.root.after(100, lambda: self.play_sound('offline', force=True))
                
                # 添加窗口闪烁效果
                self.alert_window.attributes('-topmost', True)
                self.flashing = True
                def flash_window():
                    if self.flashing and self.alert_window.winfo_exists():
                        current_bg = self.alert_window.cget('bg')
                        new_bg = 'red' if current_bg == 'white' else 'white'
                        self.alert_window.configure(bg=new_bg)
                        self.alert_window.after(500, flash_window)
                self.alert_window.configure(bg='white')
                flash_window()
                
                # 窗口关闭时停止闪烁和声音
                self.alert_window.protocol("WM_DELETE_WINDOW", lambda: [
                    self.close_alert_window(),
                    setattr(self, 'flashing', False)
                ])
                
            except Exception as e:
                print(f"创建报警窗口失败: {str(e)}")
                import traceback
                traceback.print_exc()
        
        # 确保在主线程执行
        self.root.after(0, create_window)
    def close_alert_window(self):
        """安全关闭报警窗口并停止所有效果"""
        try:
            self.flashing = False  # 停止闪烁
            if hasattr(self, 'alert_window'):
                self.alert_window.destroy()
            self._alert_open = False
            # 完全停止声音
            winsound.PlaySound(None, winsound.SND_PURGE)
        except Exception as e:
            print(f"关闭窗口时出错: {str(e)}")
    def choose_sound(self):
        """选择自定义报警声音"""
        file_path = filedialog.askopenfilename(
            title="选择声音文件",
            filetypes=[
                ("声音文件", "*.wav"),
                ("所有文件", "*.*")
            ]
        )
        if file_path:
            try:
                # 测试播放声音
                winsound.PlaySound(file_path, winsound.SND_FILENAME)
                self.sound_file = file_path
                messagebox.showinfo("成功", "声音设置已保存！")
            except Exception as e:
                messagebox.showerror("错误", f"无法播放所选声音文件：\n{str(e)}")
                self.sound_file = None
    def save_settings(self, dialog, year_var, month_var, day_var, 
                     hour_var, minute_var, mode_var, interval_var):
        """保存定时设置"""
        try:
            # 验证日期时间
            year = int(year_var.get())
            month = int(month_var.get())
            day = int(day_var.get())
            hour = int(hour_var.get())
            minute = int(minute_var.get())
            # 验证日期是否有效
            schedule_time = datetime.datetime(year, month, day, hour, minute)
            # 检查是否是过去的时间
            if schedule_time < datetime.datetime.now():
                raise ValueError("不能设置过去的时间")
            # 获取检测模式和间隔
            mode = mode_var.get()
            interval = int(interval_var.get())
            self.schedule_check(schedule_time, mode, interval)
            dialog.destroy()
            if mode == "once":
                msg = f"已设置定时检测时间: {schedule_time.strftime('%Y-%m-%d %H:%M')}"
            else:
                msg = f"已设置循环检测: 每{interval}分钟检测一次"
            messagebox.showinfo("成功", msg)
        except ValueError as e:
            messagebox.showerror("错误", str(e))
        except Exception as e:
            messagebox.showerror("错误", f"设置失败: {str(e)}")
    def create_tray_icon(self):
        """创建系统托盘图标"""
        # 创建图标
        image = Image.new('RGB', (64, 64), color='white')
        dc = ImageDraw.Draw(image)
        dc.rectangle([16, 16, 48, 48], fill='green')
        # 创建菜单
        menu = (
            pystray.MenuItem("显示主窗口", self.show_window),
            pystray.MenuItem("开始检测", self.start_check),
            pystray.MenuItem("停止检测", self.stop_check),
            pystray.Menu.SEPARATOR,
            pystray.MenuItem("退出", self.quit_app)
        )
        self.icon = pystray.Icon(
            "network_checker",
            image,
            "网络连通性检查工具",
            menu
        )
        # 在新线程中运行图标
        threading.Thread(target=self.icon.run, daemon=True).start()
    def show_window(self, icon=None):
        """显示主窗口"""
        self.root.deiconify()
        self.root.state('normal')
        self.root.lift()
    def hide_window(self):
        """处理窗口关闭事件"""
        if messagebox.askyesno("确认", "是否最小化到系统托盘？\n选择'否'将直接退出程序"):
            self.root.withdraw()
            if self.icon:
                self.icon.notify(
                    "程序已最小化到系统托盘",
                    "网络连通性检查工具"
                )
        else:
            self.quit_app()
    def quit_app(self, icon=None):
        """退出应用"""
        if messagebox.askyesno("确认退出", "确定要退出程序吗？"):
            if self.icon:
                self.icon.stop()
            self.root.quit()
    def play_sound(self, sound_type, force=False):
        """播放报警声音（增强兼容版）"""
        if not force and not self.alarm_enabled.get():
            return
        
        def play():
            try:
                print(f"[声音调试] 尝试播放声音，类型：{sound_type}")
                # 停止之前的声音
                winsound.PlaySound(None, winsound.SND_PURGE)
                
                # 优先尝试播放自定义声音
                if self.sound_file and os.path.exists(self.sound_file):
                    winsound.PlaySound(self.sound_file, 
                        winsound.SND_FILENAME | 
                        winsound.SND_ASYNC | 
                        winsound.SND_LOOP)
                    print("[声音调试] 使用自定义声音文件播放")
                    return
                
                # 系统默认声音备用方案
                try:
                    # 使用更兼容的系统提示音
                    system_sound = {
                        'offline': 'SystemExclamation',
                        'online': 'SystemAsterisk',
                        'complete': 'SystemQuestion'
                    }.get(sound_type, 'SystemExclamation')
                    winsound.PlaySound(system_sound, 
                        winsound.SND_ALIAS | 
                        winsound.SND_ASYNC | 
                        winsound.SND_LOOP)
                except Exception as e:
                    # 终极备用方案
                    frequency = 2500  # 频率
                    duration = 1000  # 持续时间
                    if sound_type == 'offline':
                        # 急促的蜂鸣声
                        for _ in range(3):
                            winsound.Beep(frequency, 300)
                            time.sleep(0.2)
                    else:
                        winsound.Beep(frequency, duration)
                
                print("[声音调试] 使用系统默认声音播放")
            except Exception as e:
                print(f"播放声音失败: {str(e)}")
                import traceback
                traceback.print_exc()
        
        self.root.after(0, play)
    def save_settings_to_file(self):
        """保存设置到文件"""
        settings = {
            'alarm_enabled': self.alarm_enabled.get(),
            'sound_file': self.sound_file
        }
        try:
            with open('settings.json', 'w') as f:
                json.dump(settings, f)
        except Exception as e:
            print(f"保存设置失败: {str(e)}")
    def load_settings_from_file(self):
        """从文件加载设置"""
        try:
            if os.path.exists('settings.json'):
                with open('settings.json', 'r') as f:
                    settings = json.load(f)
                self.alarm_enabled.set(settings.get('alarm_enabled', True))
                self.sound_file = settings.get('sound_file')
        except Exception as e:
            print(f"加载设置失败: {str(e)}")
    def on_closing(self):
        """程序关闭时的处理"""
        self.save_settings_to_file()
        self.hide_window()
    def stop_schedule(self):
        """停止定时检测"""
        schedule.clear()
        self.scheduled_time = None
        self.schedule_label.config(text="")
        self.countdown_label.config(text="")  # 清空倒计时标签
        messagebox.showinfo("提示", "已停止定时检测")
    def get_offline_items(self):
        """获取所有离线IP条目"""
        return [item for item in self.tree.get_children()
               if '离线' in self.tree.item(item)['values'][self.STATUS_COLUMN]]
    def tree_sort_column(self, col, reverse):
        """改进的通用列排序函数"""
        # 定义状态到排序键的映射
        status_order = {
            '在线': 0,
            '离线': 1,
            '待检测': 2,
            '检测失败': 3
        }
        
        # 获取所有项的数据和iid
        data = []
        for k in self.tree.get_children(''):
            value = self.tree.set(k, col)
            # 特殊处理状态列
            if col == '状态':
                sort_key = status_order.get(value.split()[0], 999)  # 取状态前缀
            elif col == 'IP':
                # 将IP转换为数字元组便于排序
                sort_key = tuple(map(int, value.split('. ')[-1].split('.')))
            elif col == '延迟':
                # 提取数字部分排序
                sort_key = float(value.rstrip('ms').strip()) if value else 0
            else:
                sort_key = value
            data.append((sort_key, k))
        
        # 使用sorted进行排序
        data.sort(key=lambda x: x[0], reverse=reverse)
        
        # 重新排列Treeview中的项
        for index, (_, k) in enumerate(data):
            self.tree.move(k, '', index)
        
        # 更新列标题箭头指示
        self.update_sort_indicator(col, reverse)
        
        # 保存当前排序状态
        self.current_sort = (col, reverse)
    
    def update_sort_indicator(self, col, reverse):
        """更新列标题的排序指示箭头"""
        # 清除所有列的箭头
        for column in self.tree['columns']:
            current_text = self.tree.heading(column)['text']
            self.tree.heading(column, text=current_text.rstrip(' ↑↓'))
        
        # 为当前排序列添加箭头
        arrow = ' ↓' if reverse else ' ↑'
        current_text = self.tree.heading(col)['text']
        self.tree.heading(col, text=current_text + arrow)

    def parse_ip_range(self, ip_str):
        """解析多种IP格式，返回生成的IP列表"""
        ips = []
        
        try:
            # 处理分号分隔的多CIDR格式
            for part in ip_str.split(';'):
                part = part.strip()
                if not part:
                    continue
                
                # 处理CIDR格式 (192.168.1.0/24)
                if '/' in part:
                    network = ipaddress.ip_network(part, strict=False)
                    for ip in network.hosts():
                        ips.append(str(ip))
                
                # 处理范围格式 (192.168.1.1-254)
                elif '-' in part:
                    base, range_part = part.rsplit('.', 1)
                    start_end = range_part.split('-')
                    if len(start_end) == 2:
                        start = int(start_end[0])
                        end = int(start_end[1])
                        for i in range(start, end+1):
                            ips.append(f"{base}.{i}")
                
                # 处理单个IP
                else:
                    if self.is_valid_ip(part):
                        ips.append(part)
            
            # 在return前添加警告
            if len(ips) > 1000:
                if not messagebox.askyesno(
                    "确认", 
                    f"即将生成{len(ips)}个IP地址，可能影响性能！\n确定要继续吗？"
                ):
                    return []
            
            return ips
        
        except Exception as e:
            print(f"解析IP地址失败: {str(e)}")
            return []

if __name__ == "__main__":
    app = NetworkCheckerApp()
    app.root.mainloop() 